Skip to content

feat(guest-agent): let Attest return the boot-time GPU attestation evidence - #1111

Merged
kvinwang merged 7 commits into
nextfrom
feat/attest-gpu-evidence
Aug 24, 2026
Merged

feat(guest-agent): let Attest return the boot-time GPU attestation evidence#1111
kvinwang merged 7 commits into
nextfrom
feat/attest-gpu-evidence

Conversation

@kvinwang

@kvinwang kvinwang commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

Verifying a GPU launch takes three things: the quote, the runtime event log, and the boot-time nvattest evidence whose digest the measured gpu-attestation event commits to. Attest returns the first two. The evidence only comes from a separate GpuInfo call.

That split is awkward for the party that needs it. A verifier issues a challenge, gets an attestation back, then has to call a different method for the bytes that the attestation it just received is what authenticates. The two responses share no framing, so nothing in the protocol says they came from the same agent or the same boot.

Fix

Attest takes a new AttestArgs with include_boottime_gpu_evidence, and AttestResponse gains boottime_gpu_evidence carrying the same bytes GpuInfo serves.

Why a new request message. Attest shared RawQuoteArgs with GetQuote and the legacy Tappd.RawQuote, neither of which has any business growing a GPU option. AttestArgs keeps report_data at field 1, so the wire format is unchanged for every existing caller, and reserves 2 and 3 — those numbers carried include_ccel and include_preimages while this RPC took a RawQuoteArgs (a55fdc8e24, 2d59979373, 3ee183bac2). Both were bools, so a new bool at either would make a pre-0.6.0 client's include_ccel = true arrive as a request for GPU evidence. The flag goes at 4.

Why boottime_ in the name. include_gpu_evidence was the first spelling, and it reads as though the evidence answers the caller's report_data. It does not — nvattest ran at boot against its own nonce. That misreading is precisely the hazard docs/design/attest-deployment.md §8 warns about, so the name now carries the answer instead of only the comment.

It is worth being explicit that sampling the GPU at attestation time would not be the stronger alternative it appears to be. An NVIDIA attestation report binds the GPU's identity and a nonce, but says nothing about which TD the device is attached to, so a fresh report can be relayed from a genuine remote GPU — deriving the nonce from the CPU quote does not help, because the relay can compute that nonce too. Only TDISP/TEE-IO device binding closes it. The proto says this at the field.

Why the RPC layer, not the platform backend. The evidence is a file the boot wrote, not a platform quote, so PlatformBackend stays unaware of it. It is opt-in, so callers that do not care about GPUs neither pay the disk read nor carry the payload.

Worker.AttestAppKey shares AttestResponse and leaves the field empty; the proto notes only DstackGuest.Attest populates it.

SDK parity is included: Rust attest_with(AttestConfig) next to attest (same bon builder get_tls_key uses), Go AttestWithOptions with Attest delegating to a zero-value AttestOptions, Python and JS defaulted parameters. No existing call site changes. All four default the field to "" when the server omits it, so a new SDK still works against an agent that predates it.

Verification

Full dstack/run-tests.sh: 0 failures, re-run after the rename. cargo clippy --workspace --all-features -- -D warnings --allow unused_variables clean; cargo fmt --check, go vet, gofmt -l, tsc --noEmit clean.

End-to-end against the simulator, with an evidence file planted at /run/nvidia-gpu-attestation/attestation.out:

request boottime_gpu_evidence
{"report_data":"1234"} ""
{"report_data":"1234","include_boottime_gpu_evidence":true} {"result_code":0,...}
GET /Attest?report_data=1234&include_boottime_gpu_evidence=true {"result_code":0,...}
GET /GpuInfo (for comparison) byte-identical to the two above

A request with no flag key at all — the shape every existing client sends — still returns 200 with the field empty.

SDK suites against the same simulator: Rust 14/14 + 9/9 tappd, Go all pass, JS 36/36. Python's suite needs the evidence-api dependency, which is not installable in my sandbox; I verified the change directly instead — sync/async signature parity holds (what tests/test_typing.py asserts), the flag is forwarded, and the field parses back. New tests cover both the flag-set and flag-omitted paths in the guest-agent and in every SDK.

Copilot AI lite review requested due to automatic review settings August 24, 2026 02:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@kvinwang kvinwang changed the title feat(guest-agent): let Attest return the GPU attestation evidence feat(guest-agent): let Attest return the boot-time GPU attestation evidence Aug 24, 2026
`Attest` takes a new `AttestArgs` with `include_gpu_evidence`, and
`AttestResponse` gains `gpu_evidence` carrying the same boot-time nvattest
bytes `GpuInfo` serves. A verifier that wants to check a GPU launch needs the
quote, the runtime event log and that evidence together; it can now get all
three in one round trip instead of pairing `Attest` with a second `GpuInfo`
call.

`Attest` gets its own request message rather than a flag on the shared
`RawQuoteArgs`, which `GetQuote` and the legacy `Tappd.RawQuote` also use and
which have no business growing a GPU option. `AttestArgs` keeps `report_data`
at field 1 so the wire format is unchanged for existing callers, and reserves
2 and 3: those numbers carried `include_ccel` and `include_preimages` while
this RPC took a `RawQuoteArgs`, and both were bools, so a new bool there would
read a pre-0.6.0 client's `include_ccel = true` as something else entirely.

The flag is handled in the RPC layer rather than the platform backend because
the evidence is a file the boot wrote, not a platform quote. It stays opt-in so
callers that do not care about GPUs neither pay the disk read nor carry the
payload.
Each SDK grows the request flag and the response field without breaking an
existing call site, using whatever that language already does for optional
arguments: Rust adds `attest_with(AttestConfig)` alongside `attest`, built with
the same `bon` builder `get_tls_key` uses; Go adds `AttestWithOptions` and
`Attest` delegates to it with a zero-value `AttestOptions`; Python and JS take
a defaulted parameter.

All four default `gpu_evidence` to an empty string when the server omits it, so
a new SDK still works against an agent that predates the field.
The attestation docs told verifiers to fetch GPU evidence from `GpuInfo`.
`Attest` now serves the same bytes, so point at both and keep the binding
procedure unchanged: the evidence is authenticated by `evidence_sha256` in the
measured `gpu-attestation` event either way, never by `report_data`.
`Attest(report_data, include_gpu_evidence = true)` reads as though the GPU
evidence answers the caller's challenge. It does not: nvattest ran at boot
against its own nonce, and the response carries a historical record.

That misreading is the specific hazard the GPU evidence design warns about --
a relying party believing a GPU figure says more than it does -- so put the
answer in the name rather than only in a comment. The proto now also states
why sampling the GPU at attestation time would not be an improvement: an
NVIDIA report binds the device and a nonce but not the TD the device is
attached to, so a fresh report can be relayed from a genuine remote GPU.
…from the backend

`PlatformBackend::attest_response` handed back an `AttestResponse` it could not
fully populate: the GPU evidence is a file the boot wrote, so every backend
wrote `boottime_gpu_evidence: String::new()` and the RPC layer patched the
struct afterwards. Three implementations had to remember the blank field, and
the platform abstraction knew about a field no platform supplies.

`attestation_for_report_data` returns a `VersionedAttestation` instead, which
is what the two neighbouring methods on the trait already do. Encoding it and
assembling the response is the RPC layer's job, so `Attest` builds its own
response in one expression and `AttestAppKey` states plainly that a key
attestation carries no machine evidence.
`attestation_for_report_data` described its argument, not its job. The method
attests the CVM, which is the distinction that matters once attesting a GPU is
also a thing the agent can be asked to do.
@kvinwang
kvinwang force-pushed the feat/attest-gpu-evidence branch from 06ced79 to 9b0c4f3 Compare August 24, 2026 03:33
@kvinwang
kvinwang merged commit 2f45f0b into next Aug 24, 2026
16 checks passed
@kvinwang
kvinwang deleted the feat/attest-gpu-evidence branch August 24, 2026 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants